<script lang="tsx">
  import { defineComponent, computed, ref, unref, reactive, onMounted, watch, nextTick, CSSProperties } from 'vue'
  import { useEventListener } from '/@/hooks/event/useEventListener'
  import { getSlot } from '/@/utils/helper/tsxHelper'

  type NumberOrNumberString = PropType<string | number | undefined>

  const props = {
    height: [Number, String] as NumberOrNumberString,
    maxHeight: [Number, String] as NumberOrNumberString,
    maxWidth: [Number, String] as NumberOrNumberString,
    minHeight: [Number, String] as NumberOrNumberString,
    minWidth: [Number, String] as NumberOrNumberString,
    width: [Number, String] as NumberOrNumberString,
    bench: {
      type: [Number, String] as NumberOrNumberString,
      default: 0,
    },
    itemHeight: {
      type: [Number, String] as NumberOrNumberString,
      required: true,
    },
    items: {
      type: Array as PropType<any[]>,
      default: () => [],
    },
  }

  const prefixCls = 'virtual-scroll'

  function convertToUnit(str: string | number | null | undefined, unit = 'px'): string | undefined {
    if (str == null || str === '') {
      return undefined
    } else if (isNaN(+str!)) {
      return String(str)
    } else {
      return `${Number(str)}${unit}`
    }
  }

  export default defineComponent({
    name: 'VirtualScroll',
    props,
    setup(props, { slots }) {
      const wrapElRef = ref<HTMLDivElement | null>(null)
      const state = reactive({
        first: 0,
        last: 0,
        scrollTop: 0,
      })

      const getBenchRef = computed(() => {
        return parseInt(props.bench as string, 10)
      })

      const getItemHeightRef = computed(() => {
        return parseInt(props.itemHeight as string, 10)
      })

      const getFirstToRenderRef = computed(() => {
        return Math.max(0, state.first - unref(getBenchRef))
      })

      const getLastToRenderRef = computed(() => {
        return Math.min((props.items || []).length, state.last + unref(getBenchRef))
      })

      const getContainerStyleRef = computed((): CSSProperties => {
        return {
          height: convertToUnit((props.items || []).length * unref(getItemHeightRef)),
        }
      })

      const getWrapStyleRef = computed((): CSSProperties => {
        const styles: Recordable<string> = {}
        const height = convertToUnit(props.height)
        const minHeight = convertToUnit(props.minHeight)
        const minWidth = convertToUnit(props.minWidth)
        const maxHeight = convertToUnit(props.maxHeight)
        const maxWidth = convertToUnit(props.maxWidth)
        const width = convertToUnit(props.width)

        if (height) styles.height = height
        if (minHeight) styles.minHeight = minHeight
        if (minWidth) styles.minWidth = minWidth
        if (maxHeight) styles.maxHeight = maxHeight
        if (maxWidth) styles.maxWidth = maxWidth
        if (width) styles.width = width
        return styles
      })

      watch([() => props.itemHeight, () => props.height], () => {
        onScroll()
      })

      function getLast(first: number): number {
        const wrapEl = unref(wrapElRef)
        if (!wrapEl) {
          return 0
        }
        const height = parseInt(props.height || 0, 10) || wrapEl.clientHeight

        return first + Math.ceil(height / unref(getItemHeightRef))
      }

      function getFirst(): number {
        return Math.floor(state.scrollTop / unref(getItemHeightRef))
      }

      function onScroll() {
        const wrapEl = unref(wrapElRef)
        if (!wrapEl) {
          return
        }
        state.scrollTop = wrapEl.scrollTop
        state.first = getFirst()
        state.last = getLast(state.first)
      }

      function renderChildren() {
        const { items = [] } = props
        return items.slice(unref(getFirstToRenderRef), unref(getLastToRenderRef)).map(genChild)
      }

      function genChild(item: any, index: number) {
        index += unref(getFirstToRenderRef)
        const top = convertToUnit(index * unref(getItemHeightRef))
        return (
          <div class={`${prefixCls}__item`} style={{ top }} key={index}>
            {getSlot(slots, 'default', { index, item })}
          </div>
        )
      }

      onMounted(() => {
        state.last = getLast(0)
        nextTick(() => {
          const wrapEl = unref(wrapElRef)
          if (!wrapEl) {
            return
          }
          useEventListener({
            el: wrapEl,
            name: 'scroll',
            listener: onScroll,
            wait: 0,
          })
        })
      })

      return () => (
        <div class={prefixCls} style={unref(getWrapStyleRef)} ref={wrapElRef}>
          <div class={`${prefixCls}__container`} style={unref(getContainerStyleRef)}>
            {renderChildren()}
          </div>
        </div>
      )
    },
  })
</script>
<style scoped lang="less">
  .virtual-scroll {
    position: relative;
    display: block;
    width: 100%;
    max-width: 100%;
    overflow: auto;
    flex: 1 1 auto;

    &__container {
      display: block;
    }

    &__item {
      position: absolute;
      right: 0;
      left: 0;
    }
  }
</style>
